﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Relay.UI.Controls.Docking.Primitives;
using System.Windows.Markup;

namespace Relay.UI.Controls.Docking {
    /// <summary>
    /// A DockManager-hosted panel which may contain one or more children separated by sizable splitters.
    /// </summary>
    [ContentProperty("Children")]
    public class SplitPanel : FrameworkElement, IFrameworkElementHost, IDockHierarchyObjectHost {
        #region Fields
        private BoundFrameworkElementCollection _children = null;
        private SplitPanelSplitter[] _splitters = new SplitPanelSplitter[0];
        private bool _resynchronizing = false;
        private bool _isDirty = true;
        private double _ratio = 0;
        private int _collapsedChildren = 0;
        private bool _floating = false;
        private SplitPanel _rootPanel = null;
        #endregion

        #region Dependency Properties
        private static readonly DependencyPropertyKey CellSizePropertyKey = DependencyProperty.RegisterAttachedReadOnly(
            "CellSize", typeof(double), typeof(SplitPanel), new FrameworkPropertyMetadata(100.0,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure));

        private static readonly DependencyPropertyKey VisibleChildrenPropertyKey = DependencyProperty.RegisterReadOnly(
            "VisibleChildren", typeof(List<FrameworkElement>), typeof(SplitPanel),
            new FrameworkPropertyMetadata(new List<FrameworkElement>()));

        /// <summary>
        /// Identifies the RelativeSize dependency property.
        /// </summary>
        public static readonly DependencyProperty CellSizeProperty = CellSizePropertyKey.DependencyProperty;

        /// <summary>
        /// Identifies the VisibleChildren dependency property.
        /// </summary>
        public static readonly DependencyProperty VisibleChildrenProperty = VisibleChildrenPropertyKey.DependencyProperty;

        /// <summary>
        /// Gets the value of the CellSize dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element from which the property value is read.</param>
        /// <returns>The CellSize property for the element.</returns>
        public static double GetCellSize(UIElement element) {
            return (double)element.GetValue(CellSizeProperty);
        }

        /// <summary>
        /// Sets the value of the CellSize dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element to which the attached property is written.</param>
        /// <param name="value">The needed CellSize value.</param>
        internal static void SetCellSize(UIElement element, double value) {
            element.SetValue(CellSizePropertyKey, value);
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes the <see cref="SplitPanel"/> class.
        /// </summary>
        static SplitPanel() {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitPanel),
                new FrameworkPropertyMetadata(typeof(SplitPanel)));

            VisibilityProperty.AddOwner(typeof(SplitPanel), new FrameworkPropertyMetadata(Visibility.Visible,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SplitPanel"/> class.
        /// </summary>
        public SplitPanel() {
            _children = new BoundFrameworkElementCollection(this,
                new Type[] { typeof(SplitPanel), typeof(DockWindowGroup) });
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the children of this collection.
        /// </summary>
        /// <value>The children.</value>
        public BoundFrameworkElementCollection Children {
            get { return _children; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="SplitPanel"/> is floating.
        /// </summary>
        /// <value><c>true</c> if floating; otherwise, <c>false</c>.</value>
        public bool Floating {
            get { return _floating; }
            internal set {
                _floating = value;
                foreach (FrameworkElement panel in Children)
                    if (panel is SplitPanel) ((SplitPanel)panel).Floating = value;
            }
        }

        /// <summary>
        /// Gets a list of visible children.
        /// </summary>
        /// <value>The visible children.</value>
        public List<FrameworkElement> VisibleChildren {
            get { return (List<FrameworkElement>)GetValue(VisibleChildrenProperty); }
            private set { SetValue(VisibleChildrenPropertyKey, value); }
        }

        /// <summary>
        /// Gets the orientation for the layout of the controls.
        /// </summary>
        /// <value>The orientation of the controls.</value>
        public Orientation Orientation {
            get {
                Dock dock = DockManager.GetDock(this);

                if (Parent is SplitPanel)
                    return (Parent as SplitPanel).Orientation == Orientation.Horizontal ? Orientation.Vertical :
                                                                                               Orientation.Horizontal;
                return (dock == Dock.Left || dock == Dock.Right) ? Orientation.Vertical : Orientation.Horizontal;
            }
        }

        internal SplitPanel RootPanel {
            get { return _rootPanel; }
        }
        #endregion

        #region Internal Methods
        internal double GetDockSize() {
            double size = 0;

            foreach (FrameworkElement element in Children) {
                if (element is DockWindowGroup) {
                    DockWindowGroup grp = element as DockWindowGroup;
                    size = Math.Max(grp.DockedSize, size);
                } else if (element is SplitPanel) {
                    SplitPanel panel = element as SplitPanel;
                    size = Math.Max(panel.GetDockSize(), size);
                }
            }

            return size;
        }

        internal double CellSizeFromSplitterValue(double value) {
            return value / _ratio;
        }

        internal double CellSizeToSplitterValue(double value) {
            return value * _ratio;
        }

        internal void MergeSplitPanel(SplitPanel panel, DockWindowGroup sourceElement, DockOverlayHitTestResult hitTest) {
            if (hitTest == DockOverlayHitTestResult.None)
                return;

            Orientation orientation = (hitTest == DockOverlayHitTestResult.Top || hitTest == DockOverlayHitTestResult.Bottom) ?
                Orientation.Vertical : Orientation.Horizontal;
            bool before = (hitTest == DockOverlayHitTestResult.Left || hitTest == DockOverlayHitTestResult.Top);
            int indexOffset = before ? 0 : 1;
            int sourceIndex = Children.IndexOf(sourceElement);

            if (sourceIndex == -1)
                throw new ArgumentException("sourceElement not found in Children collection", "sourceElement");

            double sizeAvailable = SplitPanel.GetCellSize(sourceElement);
            double totalSize = panel.GetTotalSize();

            if (orientation == Orientation) {
                if (panel.Orientation == Orientation) {
                    for (int i = panel.Children.Count - 1; i >= 0; i--) {
                        FrameworkElement child = panel.Children[i];
                        panel.Children.Remove(child);

                        double childSize = (SplitPanel.GetCellSize(child) / totalSize) * sizeAvailable;
                        SplitPanel.SetCellSize(child, childSize);
                        Children.Insert(indexOffset, child);

                        if (!before) indexOffset++;
                    }
                } else {
                    Children.Insert(indexOffset, panel);
                    SplitPanel.SetCellSize(panel, SplitPanel.GetCellSize(sourceElement));
                }
            } else {
                SplitPanel newPanel = new SplitPanel();
                Children.Insert(sourceIndex, newPanel);
                Children.Remove(sourceElement);
                SplitPanel.SetCellSize(newPanel, SplitPanel.GetCellSize(sourceElement));
                newPanel.Children.Add(sourceElement);

                if (panel.Orientation != Orientation) {
                    for (int i = panel.Children.Count - 1; i >= 0; i--) {
                        FrameworkElement child = panel.Children[i];
                        panel.Children.Remove(child);
                        newPanel.Children.Insert(0, child);

                        double childSize = (SplitPanel.GetCellSize(child) / totalSize) * sizeAvailable;
                        SplitPanel.SetCellSize(child, childSize);
                    }
                } else {
                    newPanel.Children.Insert(before ? 0 : 1, panel);
                    SplitPanel.SetCellSize(panel, SplitPanel.GetCellSize(sourceElement));
                }

                newPanel.ResynchronizeChildren();
            }
        }

        internal DockWindow[] GetAllDockWindows() {
            List<DockWindow> windows = new List<DockWindow>();
            foreach (FrameworkElement element in Children) {
                if (element is DockWindowGroup) {
                    DockWindowGroup group = element as DockWindowGroup;
                    IEnumerable<DockWindow> groupWindows = group.Items.OfType<DockWindow>();

                    windows.AddRange(groupWindows);
                } else if (element is SplitPanel) {
                    windows.AddRange(((SplitPanel)element).GetAllDockWindows());
                }
            }

            return windows.ToArray();
        }
        #endregion

        #region Private Methods
        private void UpdateRootPanel(SplitPanel root) {
            _rootPanel = root;
            foreach (FrameworkElement element in Children) {
                if (element is SplitPanel) ((SplitPanel)element).UpdateRootPanel(root);
            }
        }

        private int GetVisibleChildrenCount() {
            return _children.Where(x => x.Visibility == Visibility.Visible).Count();
        }

        private void CheckEmpty() {
            if (_children.Count == 0) {
                IDockHierarchyObjectHost parent = Parent as IDockHierarchyObjectHost;

                if (parent != null) parent.RemoveChild(this);
            }
        }

        private void ResynchronizeChildren() {
            _resynchronizing = true;
            foreach (SplitPanelSplitter splitter in _splitters)
                RemoveVisualChild(splitter);

            List<SplitPanelSplitter> splitterList = new List<SplitPanelSplitter>();

            _collapsedChildren = 0;

            for (int i = 0; i < _children.Count; i++) {
                if (i < _children.Count - 1) {
                    if (Visibility != Visibility.Visible) {
                        _collapsedChildren++;
                        continue;
                    }

                    SplitPanelSplitter splitter = new SplitPanelSplitter(_children[i], _children[i + 1], this);

                    splitterList.Add(splitter);
                    AddVisualChild(splitter);
                }
            }

            VisibleChildren = _children.Where(o => o.Visibility == Visibility.Visible).ToList();

            _splitters = splitterList.ToArray();
            _isDirty = false;
            _resynchronizing = false;
        }

        private double GetTotalSize() {
            return _children.Where(x => x.Visibility == Visibility.Visible)
                            .Sum(x => GetCellSize(x)) + ((GetVisibleChildrenCount() - 1) * DockManager.SplitterWidth);
        }

        private double GetActualSizeRatio(Size finalSize) {
            double totalSize = GetTotalSize();

            if (totalSize == 0) return 0;

            double actualAvailable = (Orientation == Orientation.Horizontal ? finalSize.Width : finalSize.Height);
            double splitterSize = (DockManager.SplitterWidth * (GetVisibleChildrenCount() - 1));
            return ((actualAvailable - splitterSize) / (totalSize - splitterSize));
        }
        #endregion

        #region Overrides
        protected override void OnVisualParentChanged(DependencyObject oldParent) {
            base.OnVisualParentChanged(oldParent);

            DependencyObject o = this;
            SplitPanel root = null;
            for (; o != null && o is SplitPanel; o = VisualTreeHelper.GetParent(o)) {
                root = o as SplitPanel;
            }

            UpdateRootPanel(root);
        }

        protected override Visual GetVisualChild(int index) {
            if (index < 0 || index >= VisualChildrenCount)
                throw new ArgumentOutOfRangeException("index");

            if (index < _children.Count)
                return _children[index];

            return _splitters[index - _children.Count];
        }

        protected override int VisualChildrenCount {
            get {
                return _children.Count * 2 - 1 - _collapsedChildren;
            }
        }

        protected override Size MeasureOverride(Size availableSize) {
            if (_isDirty && !_resynchronizing)
                ResynchronizeChildren();

            Size finalSize = new Size(double.IsPositiveInfinity(availableSize.Width)  ? 0 : availableSize.Width,
                                      double.IsPositiveInfinity(availableSize.Height) ? 0 : availableSize.Height);
            double ratio = GetActualSizeRatio(finalSize);

            bool isVisible = false;

            foreach (FrameworkElement element in _children) {
                if (element.Visibility != Visibility.Visible)
                    continue;

                isVisible = true;
                double size = Math.Max(GetCellSize(element) * ratio, DockManager.MinimumAreaSize);

                if (Orientation == Orientation.Horizontal)
                    element.Measure(new Size(size, finalSize.Height));
                else
                    element.Measure(new Size(finalSize.Width, size));
            }

            _ratio = ratio;
            if ((Visibility == Visibility.Visible) != isVisible) {
                ((IDockHierarchyObjectHost)this).MakeVisible(isVisible);
            }

            if (isVisible) {
                return finalSize;
            } else {
                return new Size();
            }
        }

        protected override Size ArrangeOverride(Size finalSize) {
            double ratio = GetActualSizeRatio(finalSize);
            double cur = 0;

            for (int i = 0; i < _children.Count; i++) {
                if (_children[i].Visibility == Visibility.Collapsed)
                    continue;

                Rect childRect = Rect.Empty;
                Rect splitterRect = Rect.Empty;
                double size = Math.Max(GetCellSize(_children[i]) * ratio, DockManager.MinimumAreaSize);

                if (Orientation == Orientation.Horizontal) {
                    childRect = new Rect(cur, 0, size, finalSize.Height);
                    splitterRect = new Rect(childRect.Right, 0, DockManager.SplitterWidth, finalSize.Height);

                    cur += childRect.Width + splitterRect.Width;
                } else {
                    childRect = new Rect(0, cur, finalSize.Width, size);
                    splitterRect = new Rect(0, childRect.Bottom, finalSize.Width, DockManager.SplitterWidth);

                    cur += childRect.Height + splitterRect.Height;
                }

                _children[i].Arrange(childRect);
                if (i < _children.Count - 1) {
                    _splitters[i].Arrange(splitterRect);
                }
            }

            return finalSize;
        }

        protected override void OnInitialized(EventArgs e) {
            base.OnInitialized(e);

            ResynchronizeChildren();
        }
        #endregion

        #region IFrameworkElementHost Members
        void IFrameworkElementHost.AddChild(FrameworkElement element) {
            AddLogicalChild(element);
            AddVisualChild(element);
            _isDirty = true;
            InvalidateMeasure();
        }

        void IFrameworkElementHost.RemoveChild(FrameworkElement element) {
            RemoveLogicalChild(element);
            RemoveVisualChild(element);
            _isDirty = true;
            InvalidateMeasure();
            CheckEmpty();
        }
        #endregion

        #region IDockHierarchyObject Members
        DockManager IDockHierarchyObject.GetDockManager() {
            IDockHierarchyObject parent = Parent as IDockHierarchyObject;
            return (parent == null) ? null : parent.GetDockManager();
        }

        IDropTarget[] IDockHierarchyObject.FindDropTargets() {
            List<IDropTarget> targets = new List<IDropTarget>();

            foreach (FrameworkElement element in Children) {
                if (element is IDropTarget)
                    targets.Add(element as IDropTarget);

                if (element is IDockHierarchyObject)
                    targets.AddRange(((IDockHierarchyObject)element).FindDropTargets());
            }

            return targets.ToArray();
        }

        void IDockHierarchyObjectHost.MakeVisible(bool show) {
            ((IDockHierarchyObjectHost)this).MakeDirty();

            IDockHierarchyObjectHost parent = Parent as IDockHierarchyObjectHost;
            if (parent != null) {
                parent.MakeDirty();
                if (show) parent.MakeVisible(true);
            }

            Visibility = show ? Visibility.Visible : Visibility.Collapsed;
        }

        void IDockHierarchyObjectHost.MakeDirty() {
            _isDirty = true;
        }

        void IDockHierarchyObjectHost.RemoveChild(FrameworkElement element) {
            _children.Remove(element);
        }
        #endregion
    }
}
